CSHARP-5959: Replace switch by method name in SerializerFinderVisitMethodCall with lookup table with MethodInfo as a key#1961
Conversation
There was a problem hiding this comment.
Pull request overview
This PR refactors LINQ3 serializer deduction for MethodCallExpression nodes by replacing a large name-based switch with a MethodInfo-keyed lookup table (plus a small dynamic fallback) to improve maintainability and correctness around overload resolution.
Changes:
- Replaced
switch (node.Method.Name)inSerializerFinderVisitor.VisitMethodCallwith aMethodInfo -> deducerregistry (including caching and generic-method normalization). - Added new
StringMethodreflection entries forstring.Equalsinstance/static variants and updated registrations accordingly. - Introduced
EnumerableMethod.IsContainsMethod(MethodInfo)to support dynamic detection ofContainsmethods.
Reviewed changes
Copilot reviewed 4 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
src/MongoDB.Driver/Linq/Linq3Implementation/SerializerFinders/SerializerFinderVisitMethodCall.cs |
Major refactor: method serializer deduction now uses a MethodInfo lookup table with dynamic fallback and caching. |
src/MongoDB.Driver/Linq/Linq3Implementation/SerializerFinders/SerializerFinderHelperMethods.cs |
Adds a TODO comment around helper-method duplication. |
src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/StringMethod.cs |
Adds reflection MethodInfos for string.Equals (instance + static) and exposes them. |
src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/EnumerableOrQueryableMethod.cs |
Removes the AnyOverloads set (now uses Any/AnyWithPredicate sets directly). |
src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/EnumerableMethod.cs |
Adds IsContainsMethod(MethodInfo) to support dynamic Contains deduction. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
|
|
||
| void DeduceCompareOrCompareToMethodSerializers() | ||
| // DeduceAddToSetMethodSerializers |
There was a problem hiding this comment.
I've left such comments so for now, for review purposes. So it would be easier to find where the implementation came from. I'll remove them before merging.
|
|
||
| void DeduceCompareOrCompareToMethodSerializers() | ||
| // DeduceAddToSetMethodSerializers | ||
| private static void DeduceAddToSetMethodSerializers(SerializerFinderVisitor visitor, MethodCallExpression expression) |
There was a problem hiding this comment.
I've moved all deducers from local method to static-class level methods, because codecov is struggling to show the code coverage for local methods. As the result I can see which deducers are not covered by tests. Probably I need to create more test cases to improve code coverage.
| var method = node.Method; | ||
| var arguments = node.Arguments; | ||
| var methodInfo = node.Method.IsGenericMethod && !node.Method.ContainsGenericParameters ? node.Method.GetGenericMethodDefinition() : node.Method; | ||
| var deducer = MethodCallSerializerDeducers.GetSerializerDeducer(methodInfo); |
There was a problem hiding this comment.
I've decided to move all deducers implementation into private static class simply to clear the "entry" point, to have this method on the very top and visible.
| private static class MethodCallSerializerDeducers | ||
| { | ||
| private static readonly Dictionary<MethodInfo, Action<SerializerFinderVisitor, MethodCallExpression>> __serializerResolvers = new(); | ||
| private static readonly ReaderWriterLockSlim __serializerResolversLock = new(); |
There was a problem hiding this comment.
Why do we not use a ConcurrentDictionary instead of the whole read-lock / upgrade-to-write / recheck dance in GetSerializerDeducer?
There was a problem hiding this comment.
Will do. Somehow I was thinking having Dictionary<> for "mostly readonly" access might be better, but apparently ConcurrentDictionary also handles this use case better then combination of Dictionary<> and ReadWriteLockSlim.
Thank you for the comment!
There was a problem hiding this comment.
Done, however instead of simply use the GetOrAdd method, I've decided to use TryGetValue, then manually call CreateDynamicSerializerDeducer and if it returns not-null then use GetOrAdd. We need this to avoid caching of null values in the serializer deducers dictionary.
|
Looks like a reasonable change to me. |
| public static IReadOnlyMethodInfoSet ReverseOverloads => __reverseOverloads; | ||
|
|
||
| // public methods | ||
| public static bool IsContainsMethod(MethodInfo method) |
There was a problem hiding this comment.
Is this going to also match String.Contains? A lot of providers typically special case excluding String.Contains as we don't want to handle looking for chars in a string in the same way we look for types in a containable list.
There was a problem hiding this comment.
Nope, String.Contains has it's pre-registered deducer (see like 216).
| [TestHelpers.MakeLambda((MyModel model) => model.Value.Equals(1f)), typeof(BooleanSerializer)], | ||
| [TestHelpers.MakeLambda((MyModel model) => model.Value.Equals(model.Other)), typeof(BooleanSerializer)], | ||
| [TestHelpers.MakeLambda((MyModel model) => float.Parse("42")), typeof(SingleSerializer)], | ||
| [TestHelpers.MakeLambda((MyModel model) => float.Parse(model.StringValue)), typeof(SingleSerializer)], |
There was a problem hiding this comment.
42 probably should have fractial parts for the float/decimal/double tests originally?
There was a problem hiding this comment.
We need non-constant here, so the PartialEvaluator will not replace this with ConstantExpression.
| static void DeduceToArrayMethodSerializers(SerializerFinderVisitor visitor, MethodCallExpression expression) | ||
| { | ||
| DeduceUnknownMethodSerializer(); | ||
| var sourceExpression = expression.Method.IsStatic ? expression.Arguments[0] : expression.Object; | ||
| visitor.DeduceCollectionAndCollectionSerializers(expression, sourceExpression); | ||
| } | ||
| } | ||
|
|
||
| bool IsDictionaryContainsKeyExpression(out Expression keyExpression) | ||
| { | ||
| if (DictionaryMethod.IsContainsKeyMethod(method)) | ||
| static void DeduceToListSerializers(SerializerFinderVisitor visitor, MethodCallExpression expression) | ||
| { | ||
| keyExpression = arguments[0]; | ||
| return true; | ||
| if (visitor.IsNotKnown(expression)) | ||
| { | ||
| var source = expression.Method.IsStatic ? expression.Arguments[0] : expression.Object; | ||
| if (visitor.IsKnown(source, out var sourceSerializer)) | ||
| { | ||
| var sourceItemSerializer = ArraySerializerHelper.GetItemSerializer(sourceSerializer); | ||
| var resultSerializer = ListSerializer.Create(sourceItemSerializer); | ||
| visitor.AddNodeSerializer(expression, resultSerializer); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Why are DeduceToArray and DeduceToList so different? Shouldn't they have similar bodies?
There was a problem hiding this comment.
It's an interesting question, but it's out of this PR scope. I'll double-check if the method was migrated/moved properly and can create a separate ticket for the investigation if you want.
| "ToList" => DeduceToListSerializers, | ||
| "ToString" => (visitor, expression) => visitor.DeduceSerializer(expression, StringSerializer.Instance), |
There was a problem hiding this comment.
why don't these have guards like the others?
There was a problem hiding this comment.
Do you mean checking if node is known already? Because DeduceSerializer is doing that for us + we do not create any new serializers here, but passing pre-created one.
| { | ||
| DeduceUnknownMethodSerializer(); | ||
| return null; |
There was a problem hiding this comment.
I believe the old code was caching this null case. We don't need to do that anymore?
There was a problem hiding this comment.
I think there is no needs to cache the negative detection. Because if SerializerFinder cannot find a serializer it means we already either on a way to throw an exception or we are about to switch to client side projection. I do not think we need to optimize those cases by cost of potentially unbound growing cache.
…thodCall with lookup table with MethodInfo as a key
Reintegrate the method handlers added upstream (ConcatArrays, EncStr*, LeftJoin, MinMaxScaler, SetUnion, window Pick overloads, and the Hash BsonBinaryData serializer tweak) into the new MethodInfo-keyed lookup table introduced by CSHARP-5959. TakeLast is already covered by SkipOrTakeOverloads.
adelinowona
left a comment
There was a problem hiding this comment.
LGTM but noted a minor typo to fix
| var foreignDocumentSerializer = fromCollection.DocumentSerializer; | ||
| var pipelineLambda = ExpressionHelper.UnquoteLambdaIfQueryableMethod(expression.Method, expression.Arguments[2]); | ||
| var pipelineLambdaLocalParameter = pipelineLambda.Parameters[0]; | ||
| var pipelineLamdbaForeignQueryableParameter = pipelineLambda.Parameters[1]; |
There was a problem hiding this comment.
typo in "lambda" on pipelineLamdbaForeignQueryableParameter.
The idea of the PR is to replace the switch by string method's name and checking of methodInfo, with the dictionary of deducers by method info. Most of the use-cases is covered by "static" registration, however there are number of use-cases where we duck-typing the method call (see CreateDynamicSerializerDeducer method), in such case we do dynamic registration as we go: so the next translation of the method will use "previously registered deducer" and do not need to go through all checks.